/* Copyright 2011 Rok Lenarčič 
 * 
 * This file is a part of MicroFn.
 * 
 * MicroFn is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * MicroFn is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.

 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/> */
package net.rlenar.microfn;

import java.lang.reflect.Array;
import java.util.Collection;
import java.util.Map;
import java.util.Set;


/**
 * This class is used for printing objects to human readable strings.
 * It won't fail on iterables that throw a runtime exception on next() method.
 * It won't fail on iterables that are infinite.
 * It won't fail if data structure nesting has loops in it.
 *
 * @author rok.lenarcic
 *
 */
public class PrettyPrint {
	/**
	 * 
	 * Maximum number of sequence elements pretty print will attempt to print.
	 */
	public static volatile int DEFAULT_MAX_SEQUENCE_ELEMENTS_PRINT = 1000;
	/**
	 * Optional override of DEFAULT_MAX_SEQUENCE_ELEMENTS_PRINT for this thread. 
	 * If the contained value is null then this mechanism is 
	 * inactive (no override, DEFAULT_MAX_SEQUENCE_ELEMENTS_PRINT). It starts with null(off) value.
	 * 
	 */
	public static final ThreadLocal<Integer> THREAD_MAX_SEQUENCE_ELEMENTS_PRINT = new ThreadLocal<Integer>();
	/**
	 * Prints the object to String.
	 * The method is not particularly fast, thus it is best used in cases of exceptions and logging data.
	 * CharSequences are printed as "value"
	 * Lists are printed as [value1, value2, value3]
	 * Arrays are printed as #[value1, value2, value3]
	 * Sets are printed as #{value1, value2, value3}
	 * Maps are printed as {key1=value1, key2=value2}
	 * Other iterables are printed as (value1, value2, value3)
	 * Other objects are printed as their toString() method returns.
	 * 
	 * @param obj the object that is printed to String
	 * @return string representation of the object
	 */
	public static String prettyPrint(Object obj) {
		return prettyPrint(obj, new Trail(), new StringBuilder()).toString();
	}
	private static StringBuilder prettyPrint(Object obj, Trail trail, StringBuilder buf) {
		if (obj == null) {
			buf.append("null");
			return buf;
		}
		int occurence = trail.contains(obj);
		if (occurence != -1) {
			buf.append("NESTING CYCLE: SAME AS LEVEL " + occurence);
			return buf;
		}
		if (obj instanceof CharSequence) {
			buf.append("\"");
			buf.append(obj.toString());
			buf.append("\"");
		} else if (obj instanceof Set<?>) {
		    buf.append('#');
			buf.append('{');
			trail.push(obj);
			Set<?> set = (Set<?>)obj;
			for (Object en : set) {
				prettyPrint(en, trail, buf);
				buf.append(", ");
			}
			if (!set.isEmpty()) {
			    buf.setLength(buf.length()-2);
			}
			buf.append('}');
			trail.pop();
		} else if (obj instanceof Collection<?>) {
			buf.append('[');
			trail.push(obj);
			Collection<?> coll = (Collection<?>)obj;
			for (Object en : coll) {
				prettyPrint(en, trail, buf);
				buf.append(", ");
			}
            if (!coll.isEmpty()) {
                buf.setLength(buf.length()-2);
            }
			buf.append(']');			
			trail.pop();
		} else if (obj instanceof Iterable<?>) {
			buf.append('(');
			trail.push(obj);
			boolean hadElement = false;
			try {
				Integer max = THREAD_MAX_SEQUENCE_ELEMENTS_PRINT.get();
				if (max == null) {
					max = DEFAULT_MAX_SEQUENCE_ELEMENTS_PRINT;
				}
				int i = 0;
				for (Object en : ((Iterable<?>)obj)) {
					if (i == max) {
						buf.append("MORE....");
						buf.append(", ");
						break;
					}
					hadElement = true;
					prettyPrint(en, trail, buf);
					buf.append(", ");
					i++;
				}
			} catch (RuntimeException e) {
				buf.append("EXC: " + e.toString());
				buf.append(", ");
			}
            if (hadElement) {
                buf.setLength(buf.length()-2);
            }
			buf.append(')');			
			trail.pop();			
		} else if (obj instanceof Map<?,?>) {
			trail.push(obj);
			buf.append('{');
			Map<?,?> m = (Map<?,?>)obj;
			for (Map.Entry<?, ?> en : m.entrySet()) {
				prettyPrint(en.getKey(), trail, buf);
				buf.append('=');
				prettyPrint(en.getValue(), trail, buf);
				buf.append(", ");
			}
            if (!m.isEmpty()) {
                buf.setLength(buf.length()-2);
            }
			buf.append('}');
			trail.pop();
		} else if (obj.getClass().isArray()) {
			int length = Array.getLength(obj);
            buf.append('#');			
			buf.append('[');
			trail.push(obj);
			for (int i = 0;i < length;i++) {
				prettyPrint(Array.get(obj, i), trail, buf);
				buf.append(", ");
			}
            if (length > 0) {
                buf.setLength(buf.length()-2);
            }
			buf.append(']');			
			trail.pop();
		} else {
			buf.append(obj.toString());
		}
		return buf;
	}
	private static final class Trail {
		private Object[] trail = new Object[10];
		private int trailLength = 0;
		public Trail push(Object o) {
			if (trailLength == trail.length) {
				Object[] newTrailArray = new Object[trail.length+10];
				System.arraycopy(trail, 0, newTrailArray, 0, trail.length);
				trail = newTrailArray;
			}
			trail[trailLength] = o;
			trailLength++;
			return this;
		}
		public Trail pop() {
			trailLength--;
			return this;
		}
		public int contains(Object o) {
			for (int i = 0;i<trailLength;i++) {
				if (o == trail[i]) {
					return i;
				}
			}
			return -1;
		}
	}
}
